IP是整个TCP/IP协议的核心,TCP、UDP、ICMP、IGMP等协议都基于IP来传送协议数据,常见的广域网路由器就工作在IP层,它们负责将IP数据报从源主机送至目的主机。

网络相关知识

  • IP是一种不可靠的无连接数据报协议,它提供尽量最大努力交付服务,但是并不保证每个分组都能被正确送达,此外,IP不会对运载的数据进行差错校验和跟踪。

  • IP地址被分为A,B,C,D,E五类,在分类地址中,IP地址由网络号和主机号组成,网络号标识某个网络,主机号标识该网络内的某台主机,这两个部分的长度是可变的,主要取决于地址的类型:对于A类地址,第一个字节定义网络号,后三个字节定义主机号;对于B类地址,前两个字节定义网络号,后两个字节定义主机号;对于C类网络地址,前三个字节定义网络号,最后一个字节定义主机号;对于D类和E类地址,不进行主机号和网络号的划分,D类地址是多播(组播)地址,而E类地址保留未用。

  • 网络地址:用来标识不同的网络,不指向具体的哪一个主机或设备,而是标识属于同一个网络的主机或网络设备的集合。当一个数据包到达一个网络时,该网络的路由器首先判断数据包的目的网络是否与本地网络号相匹配,如果两个地址不匹配,那么路由器将会根据合适的算法对数据包进行转发,只有两个地址相互匹配,路由器才会查找响应的主机号进行主机的匹配,最后将数据包发送到指定的主机。

  • 子网编址:将网络进一步划分为子网的设计,允许多个物理网络共享一个网络前缀,但每个子网都有自己的子网地址。在子网编址中,将主机号再划分为一个子网号和主机号。

  • NAT(网络地址转换)

    通常,企业内部使用的局域网路由器都是具有NAT功能的,具有NAT功能的路由器至少有一个内部端口和一个外部端口,内部端口是路由器为了与局域网内的用户通信而使用的,它使用一个内部专用的IP地址,例如常见的路由器内部IP地址为192.168.1.1,外部端口是路由器用来与外部网络通信用的,它通常具有一个有效的IP地址,例如一个有效的C类地址222.197.179.21。

    当内部网络用户连接互联网时,NAT将用户的内部IP地址转换成一个外部公共IP地址,反之,数据从外部返回时,NAT将目的地址替换成用户的内部IP地址。

    NAT的实现方式有很多种,使用最广泛的是端口多路复用,它基于TCP或UDP协议端口号以及IP地址来实现NAT功能。假如我们的局域网用户(专用地址是192.168.1.78)需要使用TCP协议与Internet中的一个Http服务器进行通信(IP地址为130.21.45.20,服务端口号是80),则它将发送一个包含源IP地址、源端口号、目的IP地址、目的端口号的IP分组到路由器处,这里假设这四个值为:(192.168.1.78, 1234, 130.21.45.20, 80),具有NAT功能的路由器会在内部维护一个NAT转换表,当路由器收到该分组时,会在表中为连接(192.168.1.78, 1234, 130.21.45.20, 80)分配一个路由器内部的NAT端口(假设为5678),另一方面,路由器在转发该分组时,会先更改分组中的源IP地址和端口号(分别改为路由器外部IP地址和NAT端口),然后才将数据包发送出去,所以被路由器转发出去的数据包中的信息为(222.178.197.21,5678,130.21.45.20,80),当服务器响应这个数据包时,它返回的数据报头部信息应为(130.21.45.20,80,222.178.197.21,5678),路由器接收到这个分组后,会在NAT转换表中查找NAT端口号为5678的连接,并把数据分组中的IP地址信号和端口信号更改为NAT转换表中记录的信息(130.21.45.20,80,192.168.1.78, 1234),最后把这个数据包返回给用户主机。

    经过NAT两次简单的转换,局域网用户就实现了与外部网络的数据包交互,但是路由器应该为每个连接分配一个唯一的NAT端口号,并及时回收那些不用的端口号。

数据报

  • IP层的数据包,更正式的叫IP数据报或IP分组,在以太网中,IP数据报和ARP报文都是组装在以太网数据帧中发送的。

  • IP数据报的组织形式:通常有两部分组成,即IP首部和数据,首部常见长度为20个字节,IP数据报对数据的格式没有规定,所以IP数据报可以用来运输任意类型的数据。

  • IP数据报的组成:

  • 版本号(4bit):包含了创建数据报所使用的IP协议版本信息,对于IPv4,该值为4,对于IPv6,该值为6

  • 首部长度(4bit):这个长度以字为单位(4字节),对于不含任何选项字段的IP首部,该长度值为5(5*4=20字节),该字段最大值为15,所以最大IP首部长度为60字节

  • 服务类型字段(8bit):Type of Service, TOS,该字段主要用于描述当前IP数据报急需的服务类型,如最小延时、最大吞吐量、最高可靠性、最小费用等,路由器在转发数据报时可以根据这个字段的值来为数据报选择最合理的路由路径

  • 总长度字段(16bit):描述了整个IP数据报(IP首部和数据区)的总字节数,理论来说,IP数据报总长度最大可达65535字节,但在实际应用中,底层链路不允许这么大的数据包出现在链路中,在以太网中,以太网数据帧中的数据长度最长为1500字节(MTU),当一个很大的IP数据报需要发送时,IP层首先要检查底层接口设备的MTU,然后将大的数据报划分为几个分片包,最后分别递交给底层发送;另一方面,当以太网数据帧中的数据量较少时,例如小于46字节,底层会在以太网数据中加入一定的填充字节以满足长度要求

  • 标识字段(16bit):用于标识IP层发送出去的每一份IP数据报,没发送一份报文,则该值加1,数据包被分片时,该字段会被复制到每一个分片中,在接收端,会使用这个字段值来组装所有分片为一个完整的数据报

  • 标志字段(3bit):第一位保留,第二位为不分片位,当该位被置位时,IP数据报在发送或转发过程中,不能进行分片,在这种情况下,如果这个数据报由于太大而不能被放在任何物理网络中进行发送,那么这个数据报会被丢弃,当该位为0时,IP层将在需要的时候对数据包进行分片处理;第三位表示更多分片位,当该位被置1时,说明该分片不是某个数据报的最后一个分片,当该位为0时,表示该分片时某个数据报的最后一个分片,或者是某个数据报的唯一分片

  • 片偏移字段(13bit):当前分片所携带的数据在整个数据报中的相对位置(以8字节为单位),目的站必须收到从0偏移量到最高偏移量的所有分片,才能将分片重装为一个完整的数据报,每个分片在网络中单独传输,它们到达终点的顺序可能会各不相同,但在目的端,将按照分片的偏移量来顺序组织各个分片

  • 生存时间(TTL):描述IP数据报最多能被转发的次数,每经过一次转发,该值会减一,当该值为0时,路由器会丢弃该分组,同时一个ICMP差错报文会返回到源主机

  • 协议字段(8bit):描述数据报中的数据是来自于哪个上层协议,该位为1表示ICMP协议,为2表示IGMP协议,为6表示TCP协议,为17表示UDP协议

  • 首部检验和(16bit):针对IP首部进行校验,它并不关心内部数据在传输过程中出错与否,对于数据的校验是上层协议负责的,如ICMP、IGMP、TCP、UDP协议都会计算它们头部以及整个数据区的校验和

  • IP层输出

  • 发送数据报

    当传输层协议(TCP或UDP)要发送数据时,它们会将数据按照自己的格式组装在一个pbuf 中,并将payload指针指向协议首部,然后调用IP层的数据报发送函数ip_output 发送数据,ip_output的调用者需要为它提供数据报首部中的目的IP地址、源IP地址、协议类型、TTL等重要信息,ip_output的函数源代码如下,它主要的工作是根据目的IP地址为该数据报选择一个合适的网络接口,然后调用函数ip_output_if将数据报发送出去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /*
    函数功能:被传输层协议调用以发送数据报,该函数查找网络接口并调用函数ip_output_if完成最终的发送工作
    @p: 传输层协议需要发送的数据包pbuf,payload指针已指向协议首部
    @src:源IP地址,若为NULL,则使用网络接口结构中保存的IP地址
    @dest:目的IP地址,若为IP_HDRINCL,则表示p中已经有了填写好的IP数据报首部,且payload指针也已经指向了IP首部
    @ttl:IP首部中的TTL字段
    @tos:IP首部中的服务类型
    @proto:IP首部中的协议类型值
    */
    err_t ip_output(struct pbuf* p, struct ip_addr* src, struct ip_addr* dest, u8_t ttl, u8_t tos, u8_t proto) {
    struct netif* netif;
    //根据目的IP地址为数据报寻找一个合适的网络接口
    if ((netif = ip_route(dest)) == NULL) {
    return ERR_RTE;
    }
    return ip_output_if(p, src, dest, ttl, tos, proto, netif);
    }
    err_t ip_output_if(struct pbuf* p, struct ip_addr* src, struct ip_addr* dest, u8_t ttl, u8_t tos, u8_t proto, struct netif* netif) {
    struct ip_hdr* iphdr;
    static u16_t ip_id = 0; // 静态变量,记录IP数据报的编号(标识字段)
    if (dest != IP_HDRINCL) {
    u16_t ip_hlen = IP_HLEN; // IP_HLEN为默认的IP首部长度,20
    if (pbuf_header(p, IP_HLEN)) { // 移动payload指针,指向pbuf中的IP首部
    return ERR_BUF;
    }
    iphdr = p->payload; // iphdr指向数据报首部
    IPH_TTL_SET(iphdr, ttl);
    IPH_PROTO_SET(iphdr, proto);
    ip_addr_set(&(iphdr->dest), dest);
    IPH_VHLTOS_SET(iphdr, 4, ip_hlen / 4, tos); // 填写版本号+首部长度+服务类型
    IPH_LEN_SET(iphdr, htons(p->tot_len)); // 填写数据报总长度
    IPH_OFFSET_SET(iphdr, 0); // 填写标志位和片偏移字段,都为0
    IPH_ID_SET(iphdr, htons(ip_id)); // 填写标识字段
    ++ip_id; // 数据报编号值加1
    if (ip_addr_isany(src)) {// 若src为空,则将源IP地址填写为网络接口的IP地址
    ip_addr_set(&(iphdr->src), &(netif->ip_addr));
    } else {
    ip_addr_set(&(iphdr->src), src);
    }
    IPH_CHKSUM_SET(iphdr, 0); // 清0校验和字段
    IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, ip_hlen)); // 计算并填写首部校验和
    } else {
    iphdr = p->payload;
    dest = &(iphdr->dest);
    }
    // 下面开始发送IP数据报,分为三种情况进行处理
    if (ip_addr_cmp(dest, &netif->ip_addr)) { // 如果目的IP地址是本网卡的地址
    return netif_loop_output(netif, p ,dest);
    }
    if (netif->mtu && (p->tot_len > netif->mtu)) {
    return ip_frag(p, netif, dest);
    }
    return netif->output(netif, p, dest);
    }

    对于最后一种情况,会调用网卡结构注册的output函数来完成数据报的发送工作,这里这个函数应为etharp_output,它会解析MAC地址,组装以太网帧并发送。